///PHILOTES Source Code.  (C)2012 PhiloLabs

#include "OgreStableHeaders.h"
#include "OgreInstanceManager.h"
#include "OgreInstanceBatchHW.h"
#include "OgreInstanceBatchHW_VTF.h"
#include "OgreInstanceBatchShader.h"
#include "OgreInstanceBatchVTF.h"
#include "OgreMesh.h"
#include "OgreSubMesh.h"
#include "OgreMeshManager.h"
#include "OgreMaterialManager.h"
#include "OgreSceneManager.h"

namespace Ogre
{
	InstanceManager::InstanceManager( const String &customName, SceneManager *sceneManager,
										const String &meshName, const String &groupName,
										InstancingTechnique instancingTechnique, uint16 instancingFlags,
										size_t instancesPerBatch, unsigned short subMeshIdx, bool useBoneMatrixLookup ) :
				mName( customName ),
				mIdCount( 0 ),
				mInstancesPerBatch( instancesPerBatch ),
				mInstancingTechnique( instancingTechnique ),
				mInstancingFlags( instancingFlags ),
				mSubMeshIdx( subMeshIdx ),
                mSceneManager( sceneManager ),
				mMaxLookupTableInstances(16)
	{
		mMeshReference = MeshManager::getSingleton().load( meshName, groupName );

		if( mMeshReference->hasSkeleton() && !mMeshReference->getSkeleton().isNull() )
			mMeshReference->getSubMesh(mSubMeshIdx)->_compileBoneAssignments();
	}

	InstanceManager::~InstanceManager()
	{
		//Remove all batches from all materials we created
		InstanceBatchMap::const_iterator itor = mInstanceBatches.begin();
		InstanceBatchMap::const_iterator end  = mInstanceBatches.end();

		while( itor != end )
		{
			InstanceBatchVec::const_iterator it = itor->second.begin();
			InstanceBatchVec::const_iterator en = itor->second.end();

			while( it != en )
				PHILO_DELETE *it++;

			++itor;
		}
	}
	//----------------------------------------------------------------------
	void InstanceManager::setInstancesPerBatch( size_t instancesPerBatch )
	{
		if( !mInstanceBatches.empty() )
		{
			PHILO_EXCEPT(Exception::ERR_INVALID_STATE, "Instances per batch can only be changed before"
						" building the batch.", "InstanceManager::setInstancesPerBatch");
		}

		mInstancesPerBatch = instancesPerBatch;
	}

	//----------------------------------------------------------------------
	void InstanceManager::setMaxLookupTableInstances( size_t maxLookupTableInstances )
	{
		if( !mInstanceBatches.empty() )
		{
			PHILO_EXCEPT(Exception::ERR_INVALID_STATE, "Instances per batch can only be changed before"
				" building the batch.", "InstanceManager::setInstancesPerBatch");
		}

		mMaxLookupTableInstances = maxLookupTableInstances;
	}
	
	//----------------------------------------------------------------------
	size_t InstanceManager::getMaxOrBestNumInstancesPerBatch( String materialName, size_t suggestedSize,
																uint16 flags )
	{
		//Get the material
		MaterialPtr mat = MaterialManager::getSingleton().getByName( materialName,
																	mMeshReference->getGroup() );
		InstanceBatch *batch = 0;

		//Base material couldn't be found
		if( mat.isNull() )
			return 0;

		switch( mInstancingTechnique )
		{
		case ShaderBased:
			batch = PHILO_NEW InstanceBatchShader( this, mMeshReference, mat, suggestedSize,
													0, mName + "/TempBatch" );
			break;
		case TextureVTF:
			batch = PHILO_NEW InstanceBatchVTF( this, mMeshReference, mat, suggestedSize,
													0, mName + "/TempBatch" );
			static_cast<InstanceBatchVTF*>(batch)->setBoneDualQuaternions(mInstancingFlags & IM_USEBONEDUALQUATERNIONS);
			static_cast<InstanceBatchVTF*>(batch)->setUseOneWeight(mInstancingFlags & IM_USEONEWEIGHT);
			static_cast<InstanceBatchVTF*>(batch)->setForceOneWeight(mInstancingFlags & IM_FORCEONEWEIGHT);
			break;
		case HWInstancingBasic:
			batch = PHILO_NEW InstanceBatchHW( this, mMeshReference, mat, suggestedSize,
													0, mName + "/TempBatch" );
			break;
		case HWInstancingVTF:
			batch = PHILO_NEW InstanceBatchHW_VTF( this, mMeshReference, mat, suggestedSize,
													0, mName + "/TempBatch" );
			static_cast<InstanceBatchHW_VTF*>(batch)->setBoneMatrixLookup(mInstancingFlags & IM_VTFBONEMATRIXLOOKUP, mMaxLookupTableInstances);
			static_cast<InstanceBatchHW_VTF*>(batch)->setBoneDualQuaternions(mInstancingFlags & IM_USEBONEDUALQUATERNIONS);
			static_cast<InstanceBatchHW_VTF*>(batch)->setUseOneWeight(mInstancingFlags & IM_USEONEWEIGHT);
			static_cast<InstanceBatchHW_VTF*>(batch)->setForceOneWeight(mInstancingFlags & IM_FORCEONEWEIGHT);
			break;
		default:
			PHILO_EXCEPT(Exception::ERR_NOT_IMPLEMENTED,
					"Unimplemented instancing technique: " +
					StringConverter::toString(mInstancingTechnique),
					"InstanceBatch::getMaxOrBestNumInstancesPerBatches()");
		}

		const size_t retVal = batch->calculateMaxNumInstances( mMeshReference->getSubMesh(mSubMeshIdx),
																flags );

		PHILO_DELETE batch;

		return retVal;
	}
	//----------------------------------------------------------------------
	InstancedEntity* InstanceManager::createInstancedEntity( const String &materialName )
	{
		InstanceBatch *instanceBatch;

		if( mInstanceBatches.empty() )
			instanceBatch = buildNewBatch( materialName, true );
		else
			instanceBatch = getFreeBatch( materialName );

		return instanceBatch->createInstancedEntity();
	}
	//-----------------------------------------------------------------------
	inline InstanceBatch* InstanceManager::getFreeBatch( const String &materialName )
	{
		InstanceBatchVec &batchVec = mInstanceBatches[materialName];

		InstanceBatchVec::const_reverse_iterator itor = batchVec.rbegin();
		InstanceBatchVec::const_reverse_iterator end  = batchVec.rend();

		while( itor != end )
		{
			if( !(*itor)->isBatchFull() )
				return *itor;
			++itor;
		}

		//None found, or they're all full
		return buildNewBatch( materialName, false );
	}
	//-----------------------------------------------------------------------
	InstanceBatch* InstanceManager::buildNewBatch( const String &materialName, bool firstTime )
	{
		//Get the bone to index map for the batches
		Mesh::IndexMap &idxMap = mMeshReference->getSubMesh(mSubMeshIdx)->blendIndexToBoneIndexMap;
		idxMap = idxMap.empty() ? mMeshReference->sharedBlendIndexToBoneIndexMap : idxMap;

		//Get the material
		MaterialPtr mat = MaterialManager::getSingleton().getByName( materialName,
																	mMeshReference->getGroup() );

		//Get the array of batches grouped by this material
		InstanceBatchVec &materialInstanceBatch = mInstanceBatches[materialName];

		InstanceBatch *batch = 0;

		switch( mInstancingTechnique )
		{
		case ShaderBased:
			batch = PHILO_NEW InstanceBatchShader( this, mMeshReference, mat, mInstancesPerBatch,
													&idxMap, mName + "/InstanceBatch_" +
													StringConverter::toString(mIdCount++) );
			break;
		case TextureVTF:
			batch = PHILO_NEW InstanceBatchVTF( this, mMeshReference, mat, mInstancesPerBatch,
													&idxMap, mName + "/InstanceBatch_" +
													StringConverter::toString(mIdCount++) );
			static_cast<InstanceBatchVTF*>(batch)->setBoneDualQuaternions(mInstancingFlags & IM_USEBONEDUALQUATERNIONS);
			static_cast<InstanceBatchVTF*>(batch)->setUseOneWeight(mInstancingFlags & IM_USEONEWEIGHT);
			static_cast<InstanceBatchVTF*>(batch)->setForceOneWeight(mInstancingFlags & IM_FORCEONEWEIGHT);
			break;
		case HWInstancingBasic:
			batch = PHILO_NEW InstanceBatchHW( this, mMeshReference, mat, mInstancesPerBatch,
													&idxMap, mName + "/InstanceBatch_" +
													StringConverter::toString(mIdCount++) );
			break;
		case HWInstancingVTF:
			batch = PHILO_NEW InstanceBatchHW_VTF( this, mMeshReference, mat, mInstancesPerBatch,
													&idxMap, mName + "/InstanceBatch_" +
													StringConverter::toString(mIdCount++) );
			static_cast<InstanceBatchHW_VTF*>(batch)->setBoneMatrixLookup(mInstancingFlags & IM_VTFBONEMATRIXLOOKUP, mMaxLookupTableInstances);
			static_cast<InstanceBatchHW_VTF*>(batch)->setBoneDualQuaternions(mInstancingFlags & IM_USEBONEDUALQUATERNIONS);
			static_cast<InstanceBatchHW_VTF*>(batch)->setUseOneWeight(mInstancingFlags & IM_USEONEWEIGHT);
			static_cast<InstanceBatchHW_VTF*>(batch)->setForceOneWeight(mInstancingFlags & IM_FORCEONEWEIGHT);
			break;
		default:
			PHILO_EXCEPT(Exception::ERR_NOT_IMPLEMENTED,
					"Unimplemented instancing technique: " +
					StringConverter::toString(mInstancingTechnique),
					"InstanceBatch::buildNewBatch()");
		}

		batch->_notifyManager( mSceneManager );


		if( !firstTime )
		{
			//TODO: Check different materials have the same mInstancesPerBatch upper limit
			//otherwise we can't share
			batch->buildFrom( mMeshReference->getSubMesh(mSubMeshIdx), mSharedRenderOperation );
		}
		else
		{
			//Ensure we don't request more than we can
			const size_t maxInstPerBatch = batch->calculateMaxNumInstances( mMeshReference->
														getSubMesh(mSubMeshIdx), mInstancingFlags );
			mInstancesPerBatch = std::min( maxInstPerBatch, mInstancesPerBatch );
			batch->_setInstancesPerBatch( mInstancesPerBatch );

			//TODO: Create a "merge" function that merges all submeshes into one big submesh
			//instead of just sending submesh #0

			//Get the RenderOperation to be shared with further instances.
			mSharedRenderOperation = batch->build( mMeshReference->getSubMesh(mSubMeshIdx) );
		}

		const BatchSettings &batchSettings = mBatchSettings[materialName];
		batch->setCastShadows( batchSettings.setting[CAST_SHADOWS] );

		//Batches need to be part of a scene node so that their renderable can be rendered
		SceneNode *sceneNode = mSceneManager->getRootSceneNode()->createChildSceneNode();
		sceneNode->attachObject( batch );
		sceneNode->showBoundingBox( batchSettings.setting[SHOW_BOUNDINGBOX] );

		materialInstanceBatch.push_back( batch );

		return batch;
	}
	//-----------------------------------------------------------------------
	void InstanceManager::cleanupEmptyBatches(void)
	{
		//Do this now to avoid any dangling pointer inside mDirtyBatches
		_updateDirtyBatches();

		InstanceBatchMap::iterator itor = mInstanceBatches.begin();
		InstanceBatchMap::iterator end  = mInstanceBatches.end();

		while( itor != end )
		{
			InstanceBatchVec::iterator it = itor->second.begin();
			InstanceBatchVec::iterator en = itor->second.end();

			while( it != en )
			{
				if( (*it)->isBatchUnused() )
				{
					PHILO_DELETE *it;
					//Remove it from the list swapping with the last element and popping back
					size_t idx = it - itor->second.begin();
					*it = itor->second.back();
					itor->second.pop_back();

					//Restore invalidated iterators
					it = itor->second.begin() + idx;
					en = itor->second.end();
				}
				else
					++it;
			}

			++itor;
		}

		//By this point it may happen that all mInstanceBatches' objects are also empty
		//however if we call mInstanceBatches.clear(), next time we'll create an InstancedObject
		//we'll end up calling buildFirstTime() instead of buildNewBatch(), which is not the idea
		//(takes more time and will leak the shared render operation)
	}
	//-----------------------------------------------------------------------
	void InstanceManager::defragmentBatches( bool optimizeCull,
												InstanceBatch::InstancedEntityVec &usedEntities,
												InstanceBatchVec &fragmentedBatches )
	{
		InstanceBatchVec::iterator itor = fragmentedBatches.begin();
		InstanceBatchVec::iterator end  = fragmentedBatches.end();

		while( itor != end && !usedEntities.empty() )
		{
			if( !(*itor)->isStatic() )
				(*itor)->_defragmentBatch( optimizeCull, usedEntities );
			++itor;
		}

		InstanceBatchVec::iterator lastImportantBatch = itor;

		while( itor != end )
		{
			if( !(*itor)->isStatic() )
			{
				//If we get here, this means we hit remaining batches which will be unused.
				//Destroy them
				//Call this to avoid freeing InstancedEntities that were just reparented
				(*itor)->_defragmentBatchDiscard();
				PHILO_DELETE *itor;
			}
			else
			{
				//This isn't a meaningless batch, move it forward so it doesn't get wipe
				//when we resize the container (faster than removing element by element)
				*lastImportantBatch++ = *itor;
			}

			++itor;
		}

		//Remove remaining batches all at once from the vector
		const size_t remainingBatches = end - lastImportantBatch;
		fragmentedBatches.resize( fragmentedBatches.size() - remainingBatches );
	}
	//-----------------------------------------------------------------------
	void InstanceManager::defragmentBatches( bool optimizeCulling )
	{
		//Do this now to avoid any dangling pointer inside mDirtyBatches
		_updateDirtyBatches();

		//Do this for every material
		InstanceBatchMap::iterator itor = mInstanceBatches.begin();
		InstanceBatchMap::iterator end  = mInstanceBatches.end();

		while( itor != end )
		{
			InstanceBatch::InstancedEntityVec usedEntities;
			usedEntities.reserve( itor->second.size() * mInstancesPerBatch );

			//Collect all Instanced Entities being used by _all_ batches from this material
			InstanceBatchVec::iterator it = itor->second.begin();
			InstanceBatchVec::iterator en = itor->second.end();

			while( it != en )
			{
				//Don't collect instances from static batches, we assume they're correctly set
				//Plus, we don't want to put InstancedEntities from non-static into static batches
				if( !(*it)->isStatic() )
					(*it)->getInstancedEntitiesInUse( usedEntities );
				++it;
			}

			defragmentBatches( optimizeCulling, usedEntities, itor->second );

			++itor;
		}
	}
	//-----------------------------------------------------------------------
	void InstanceManager::setSetting( BatchSettingId id, bool value, const String &materialName )
	{
		assert( id < NUM_SETTINGS );

		if( materialName == StringUtil::BLANK )
		{
			//Setup all existing materials
			InstanceBatchMap::iterator itor = mInstanceBatches.begin();
			InstanceBatchMap::iterator end  = mInstanceBatches.end();

			while( itor != end )
			{
				mBatchSettings[itor->first].setting[id] = value;
				applySettingToBatches( id, value, itor->second );

				++itor;
			}
		}
		else
		{
			//Setup a given material
			mBatchSettings[materialName].setting[id] = value;

			InstanceBatchMap::const_iterator itor = mInstanceBatches.find( materialName );
			//Don't crash or throw if the batch with that material hasn't been created yet
			if( itor != mInstanceBatches.end() )
				applySettingToBatches( id, value, itor->second );
		}
	}
	//-----------------------------------------------------------------------
	bool InstanceManager::getSetting( BatchSettingId id, const String &materialName ) const
	{
		assert( id < NUM_SETTINGS );

		BatchSettingsMap::const_iterator itor = mBatchSettings.find( materialName );
		if( itor != mBatchSettings.end() )
			return itor->second.setting[id]; //Return current setting

		//Return default
		return BatchSettings().setting[id];
	}
	//-----------------------------------------------------------------------
	void InstanceManager::applySettingToBatches( BatchSettingId id, bool value,
												 const InstanceBatchVec &container )
	{
		InstanceBatchVec::const_iterator itor = container.begin();
		InstanceBatchVec::const_iterator end  = container.end();

		while( itor != end )
		{
			switch( id )
			{
			case CAST_SHADOWS:
				(*itor)->setCastShadows( value );
				break;
			case SHOW_BOUNDINGBOX:
				(*itor)->getParentSceneNode()->showBoundingBox( value );
				break;
			default:
				break;
			}
			++itor;
		}
	}
	//-----------------------------------------------------------------------
	void InstanceManager::setBatchesAsStaticAndUpdate( bool bStatic )
	{
		InstanceBatchMap::iterator itor = mInstanceBatches.begin();
		InstanceBatchMap::iterator end  = mInstanceBatches.end();

		while( itor != end )
		{
			InstanceBatchVec::iterator it = itor->second.begin();
			InstanceBatchVec::iterator en = itor->second.end();

			while( it != en )
			{
				(*it)->setStaticAndUpdate( bStatic );
				++it;
			}

			++itor;
		}
	}
	//-----------------------------------------------------------------------
	void InstanceManager::_addDirtyBatch( InstanceBatch *dirtyBatch )
	{
		if( mDirtyBatches.empty() )
			mSceneManager->_addDirtyInstanceManager( this );

		mDirtyBatches.push_back( dirtyBatch );
	}
	//-----------------------------------------------------------------------
	void InstanceManager::_updateDirtyBatches(void)
	{
		InstanceBatchVec::const_iterator itor = mDirtyBatches.begin();
		InstanceBatchVec::const_iterator end  = mDirtyBatches.end();

		while( itor != end )
		{
			(*itor)->_updateBounds();
			++itor;
		}

		mDirtyBatches.clear();
	}
}
